package com.ihr360.payroll.service.staff.impl;

import com.google.common.collect.Lists;
import com.ihr360.commons.context.session.Ihr360SessionContextHolder;
import com.ihr360.commons.exception.Ihr360Exception;
import com.ihr360.commons.exception.Ihr360RuntimeException;
import com.ihr360.commons.lang.CollectionUtils;
import com.ihr360.commons.lang.DateUtils;
import com.ihr360.commons.lang.StringUtils;
import com.ihr360.commons.utils.JpaCriteriaPathUtils;
import com.ihr360.commons.vo.PageData;
import com.ihr360.commons.vo.ResultInfo;
import com.ihr360.payroll.dao.StaffPayrollInfoJournalRepository;
import com.ihr360.payroll.entity.staff.StaffPayrollInfoJournalEntity;
import com.ihr360.payroll.model.dto.salary.convertor.StaffPayrollInfoJournalConvertor;
import com.ihr360.payroll.model.dto.staff.StaffPayrollInfoJournal;
import com.ihr360.payroll.model.vo.StaffPayrollInfoJournalVO;
import com.ihr360.payroll.request.StaffPayrollInfoJournalRequest;
import com.ihr360.payroll.response.StaffPayrollInfoPeriodResponse;
import com.ihr360.payroll.response.StaffPayrollInfoTimelineResponse;
import com.ihr360.payroll.service.staff.StaffPayrollInfoJournalService;
import com.ihr360.payroll.type.SalaryProfileChangeType;
import com.ihr360.rest.reposity.SimpleRestReposity;
import com.ihr360.rest.service.AbstractSimpleRestServiceImpl;
import com.ihr360.shared.basicstaffinfo.dao.SyncedStaffInfoRepository;
import com.ihr360.shared.basicstaffinfo.dto.SyncedBasicStaffInfo;
import com.ihr360.validation.ValidationUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author richey
 */
@Service
public class StaffPayrollInfoJournalServiceImpl extends AbstractSimpleRestServiceImpl<StaffPayrollInfoJournal, Long> implements StaffPayrollInfoJournalService {

    @Autowired
    private StaffPayrollInfoJournalRepository staffPayrollInfoJournalRepository;

    @Autowired
    private StaffPayrollInfoJournalConvertor staffPayrollInfoJournalConvertor;

    @Autowired
    private SyncedStaffInfoRepository syncedStaffInfoRepository;

    @Override
    public SimpleRestReposity<StaffPayrollInfoJournal, Long> getSimpleRestReposity() {
        return staffPayrollInfoJournalRepository;
    }

    @Override
    public List<StaffPayrollInfoJournal> findEffectiveAfterTodayJournals(Set<String> staffIds) {

        ValidationUtils.rejectIfEmpty(staffIds, null, "员工Id不能为空！");
        String companyId = Ihr360SessionContextHolder.getCompanyId();
        List<StaffPayrollInfoJournalVO> journalParams = staffPayrollInfoJournalRepository.findFinalUpdateJournal(companyId, staffIds);
        if (CollectionUtils.isEmpty(journalParams)) {
            return Lists.newArrayList();
        }
        Specification<StaffPayrollInfoJournal> afterTodayJournalSpecification = getAfterTodayStaffPayrollJournal(companyId, journalParams);
        List<StaffPayrollInfoJournal> staffPayrollInfoJournals = staffPayrollInfoJournalRepository.findAll(afterTodayJournalSpecification);
        return staffPayrollInfoJournals;
    }

    @Override
    public StaffPayrollInfoTimelineResponse getPayrollInfoNearestTimes(String staffId, String code, String newValue, SalaryProfileChangeType changeType) {
        StaffPayrollInfoTimelineResponse response = new StaffPayrollInfoTimelineResponse();
        if (StringUtils.isEmpty(staffId) || StringUtils.isEmpty(code)) {
            throw new Ihr360RuntimeException("参数异常staffId和code不为空！");
        }
        String companyId = Ihr360SessionContextHolder.getCompanyId();
        newValue = StringUtils.isEmpty(newValue) ? null : newValue.trim();
        Date currentDay = DateUtils.getMinDateInDay(new Date());

        Pageable effectiveAtDesc = new PageRequest(0, 1, Sort.Direction.DESC, "effectiveAt");
        Page<StaffPayrollInfoJournal> currentJournalPage = staffPayrollInfoJournalRepository.findCurrentEffectiveJournal(companyId, staffId, code, DateUtils.getMaxDateInDay(new Date()), changeType, effectiveAtDesc);
        ValidationUtils.rejectIfEmpty(currentJournalPage.getContent(), null, "数据异常，未找到薪资档案调整记录");
        StaffPayrollInfoJournal currentJournal = currentJournalPage.getContent().get(0);
        Date currentCreatedAt = currentJournal.getCreatedAt();
        StaffPayrollInfoPeriodResponse currentResponse =
                new StaffPayrollInfoPeriodResponse(currentJournal.getEffectiveAt(), null, currentJournal.getNewValue());
        response.setCurrentPayrollInfo(currentResponse);

        Page<StaffPayrollInfoJournal> preJournalPage = staffPayrollInfoJournalRepository.findPreEffectiveJournal(companyId, staffId, code, newValue, currentDay, effectiveAtDesc);
        if (CollectionUtils.isNotEmpty(preJournalPage.getContent())) {
            StaffPayrollInfoJournal preJournal = preJournalPage.getContent().get(0);
            StaffPayrollInfoPeriodResponse preResponse =
                    new StaffPayrollInfoPeriodResponse(preJournal.getEffectiveAt(), DateUtils.subDays(currentJournal.getEffectiveAt(), 1), preJournal.getNewValue());
            response.setPrePayrollInfo(preResponse);
        }
        List<StaffPayrollInfoJournal> nextJournals = staffPayrollInfoJournalRepository.findNextEffectiveJournal(companyId, staffId, code, currentDay, currentCreatedAt);
        //如果相同类型，取当月之外最近月的最后一次修改
        //如果不同类型，比较每个类型中最近月最后一次修改
        if (CollectionUtils.isNotEmpty(nextJournals)) {
            StaffPayrollInfoJournal nextJournal = getNextStaffPayrollInfoJournal(currentJournal, nextJournals);
            Date invalidAt = null;
            if (nextJournal != null) {
                nextJournals = nextJournals.stream()
                        .filter(j -> isNextJournal(j, nextJournal))
                        .collect(Collectors.toList());
                if (CollectionUtils.isNotEmpty(nextJournals)) {
                    StaffPayrollInfoJournal afterNextJournal = getNextStaffPayrollInfoJournal(nextJournal, nextJournals);
                    if (afterNextJournal != null) {
                        invalidAt = afterNextJournal.getEffectiveAt();
                    }
                }
            }
            StaffPayrollInfoPeriodResponse nextResponse =
                    new StaffPayrollInfoPeriodResponse(nextJournal.getEffectiveAt(), invalidAt, nextJournal.getNewValue());
            currentResponse.setInValidAt(DateUtils.subDays(nextJournal.getEffectiveAt(), 1));
            response.setNextPayrollInfo(nextResponse);
        }
        return response;
    }

    private boolean isNextJournal(StaffPayrollInfoJournal j, StaffPayrollInfoJournal currentJournal) {
        return j.getCreatedAt().compareTo(currentJournal.getCreatedAt()) >= 0
                && j.getEffectiveAt().compareTo(currentJournal.getEffectiveAt()) > 0;
    }

    private StaffPayrollInfoJournal getNextStaffPayrollInfoJournal(StaffPayrollInfoJournal currentJournal, List<StaffPayrollInfoJournal> nextJournalPage) {
        List<StaffPayrollInfoJournal> validJournals = Lists.newArrayList();
        for (StaffPayrollInfoJournal journal : nextJournalPage) {
            boolean validSameType = journal.getChangeType().equals(currentJournal.getChangeType())
                    && !DateUtils.isSameMonth(journal.getEffectiveAt(), currentJournal.getEffectiveAt());
            boolean validNotSameType = !journal.getChangeType().equals(currentJournal.getChangeType());
            if (validSameType || validNotSameType) {
                validJournals.add(journal);
            }
        }
        StaffPayrollInfoJournal nextJournal = null;
        if (CollectionUtils.isNotEmpty(validJournals)) {
            Date minEffectiveMonth = validJournals.stream()
                    .map(j -> DateUtils.dateFormat(j.getEffectiveAt(), DateUtils.STANDARD_DATE_FORMAT_YM1))
                    .min(Comparator.naturalOrder())
                    .get();

            nextJournal = validJournals.stream()
                    .filter(s -> DateUtils.isSameMonth(minEffectiveMonth, s.getEffectiveAt()))
                    .max(Comparator.comparing(j -> j.getCreatedAt())).get();

        }
        return nextJournal;
    }

    @Override
    public PageData<StaffPayrollInfoJournalEntity> findAllJournals(StaffPayrollInfoJournalRequest param) {
        String companyId = Ihr360SessionContextHolder.getCompanyId();
        Date startEffectiveAt = param.getStartEffectiveAt();
        Date endEffectiveAt = param.getEndEffectiveAt();
        String fuzzyParam = param.getFuzzyParam();
        List<String> authStaffIds = param.getAuthStaffIds();
        if (CollectionUtils.isEmpty(authStaffIds)) {
            return new PageData<>(0, 0, null);
        }


        boolean effectiveAtInValid = startEffectiveAt != null
                && endEffectiveAt != null
                && startEffectiveAt.after(endEffectiveAt);
        if (effectiveAtInValid) {
            throw new Ihr360Exception(null, "开始生效时间不能小于结束生效时间！");
        }
        List<String> fuzzyStaffIds = Lists.newArrayList();
        if (StringUtils.isNotEmpty(fuzzyParam)) {
            Specification<SyncedBasicStaffInfo> fuzzyStaffInfo = getFuzzyStaffInfo(companyId, "%" + fuzzyParam + "%");
            List<SyncedBasicStaffInfo> basicStaffInfos = syncedStaffInfoRepository.findAll(fuzzyStaffInfo);
            if (CollectionUtils.isEmpty(basicStaffInfos)) {
                return new PageData<>(0, 0, Lists.newArrayList());
            }
            fuzzyStaffIds = basicStaffInfos.stream().map(SyncedBasicStaffInfo::getId).collect(Collectors.toList());


        }
        Specification<StaffPayrollInfoJournal> periodStaffPayrollJournal =
                getPeriodStaffPayrollJournal(companyId, startEffectiveAt, endEffectiveAt, fuzzyStaffIds, authStaffIds);
        Page<StaffPayrollInfoJournal> journals = staffPayrollInfoJournalRepository.findAll(periodStaffPayrollJournal, param.getPageable());


        if (CollectionUtils.isEmpty(journals.getContent())) {
            return new PageData<>(journals.getTotalPages(), journals.getTotalElements(), Lists.newArrayList());
        }
        List<StaffPayrollInfoJournalEntity> entities = staffPayrollInfoJournalConvertor.convertDtosToEntities(journals.getContent());
        Set<String> staffIds = entities.stream().map(StaffPayrollInfoJournalEntity::getStaffId).collect(Collectors.toSet());
        if (CollectionUtils.isEmpty(staffIds)) {
            return new PageData<>(journals.getTotalPages(), journals.getTotalElements(), entities);
        }
        List<SyncedBasicStaffInfo> staffInfolist = syncedStaffInfoRepository.findByCompanyIdAndIdIn(companyId, new ArrayList<>(staffIds));
        Map<String, SyncedBasicStaffInfo> staffInfoMap = staffInfolist
                .stream().collect(Collectors.toMap(SyncedBasicStaffInfo::getId, Function.identity()));

        entities.stream().filter(s -> StringUtils.isNotEmpty(s.getStaffId())).forEach(entity -> {
            SyncedBasicStaffInfo staffInfo = staffInfoMap.get(entity.getStaffId());
            if (staffInfo != null) {
                entity.setStaffName(staffInfo.getStaffName());
                entity.setMobileNo(staffInfo.getMobileNo());
                entity.setDepartmentName(staffInfo.getDepartmentName());
                entity.setEnrollInDate(staffInfo.getEnrollInDate());
            }
        });

        return new PageData<>(journals.getTotalPages(), journals.getTotalElements(), entities);

    }

    @Override
    public List<StaffPayrollInfoJournalEntity> getStaffJournalDetail(String staffId) {
        ValidationUtils.rejectIfEmpty(staffId, null, "员工Id不能为空!");
        String companyId = Ihr360SessionContextHolder.getCompanyId();
        List<StaffPayrollInfoJournal> journals = staffPayrollInfoJournalRepository.findByCompanyIdAndStaffIdIn(companyId, Lists.newArrayList(staffId));
        return staffPayrollInfoJournalConvertor.convertDtosToEntities(journals);
    }

    @Transactional
    @Override
    public ResultInfo save(List<StaffPayrollInfoJournalEntity> journalToSave) {
        ValidationUtils.rejectIfEmpty(journalToSave, null, "员工薪资档案调整记录不能为空！");
        List<StaffPayrollInfoJournal> journals = staffPayrollInfoJournalConvertor.convertEntitiesToDtos(journalToSave);
        staffPayrollInfoJournalRepository.save(journals);
        return new ResultInfo.Builder().success();
    }

    private Specification<StaffPayrollInfoJournal> getAfterTodayStaffPayrollJournal(String companyId, List<StaffPayrollInfoJournalVO> journalParams) {
        if (CollectionUtils.isEmpty(journalParams)) {
            throw new Ihr360RuntimeException("参数异常，journalParams不能为空！");
        }

        return (Root<StaffPayrollInfoJournal> root, CriteriaQuery<?> q, CriteriaBuilder cb) -> {
            Predicate predicate = cb.conjunction();
            predicate.getExpressions().add(cb.equal(root.get("companyId").as(String.class), companyId));
            Expression staffId = JpaCriteriaPathUtils.getPath(root, "staffId").as(String.class);
            Expression createdAt = JpaCriteriaPathUtils.getPath(root, "createdAt").as(Date.class);
            Expression fieldCode = JpaCriteriaPathUtils.getPath(root, "fieldCode").as(String.class);
            Expression effectiveAt = JpaCriteriaPathUtils.getPath(root, "effectiveAt").as(Date.class);

            List<Predicate> journalParamPredicates = Lists.newArrayList();
            Date currentDate = DateUtils.getMaxDateInDay(new Date());
            journalParams.forEach(journal -> {
                Predicate staffEqualsPredicate = cb.equal(staffId, journal.getStaffId());
                Predicate createdAtEqualsPredicate = cb.equal(createdAt, journal.getCreatedAt());
                Predicate fieldCodeEqualsPredicate = cb.equal(fieldCode, journal.getFieldCode());
                Predicate effectiveAtPredicate = cb.greaterThan(effectiveAt, currentDate);
                journalParamPredicates.add(cb.or(cb.and(staffEqualsPredicate, createdAtEqualsPredicate, fieldCodeEqualsPredicate, effectiveAtPredicate)));
            });
            Predicate p = cb.or(journalParamPredicates.toArray(new Predicate[journalParamPredicates.size()]));
            predicate.getExpressions().add(p);
            q.where(predicate);
            return q.getRestriction();
        };
    }

    private Specification<StaffPayrollInfoJournal> getPeriodStaffPayrollJournal(String companyId, Date startEffectiveAt, Date endEffectiveAt, List<String> fuzzyStaffIds, List<String> authStaffIds) {
        return (Root<StaffPayrollInfoJournal> root, CriteriaQuery<?> q, CriteriaBuilder cb) -> {
            Predicate predicate = cb.conjunction();
            predicate.getExpressions().add(cb.equal(root.get("companyId").as(String.class), companyId));
            Expression effectiveAt = JpaCriteriaPathUtils.getPath(root, "effectiveAt").as(Date.class);
            Expression staffId = JpaCriteriaPathUtils.getPath(root, "staffId").as(String.class);

            if (CollectionUtils.isNotEmpty(fuzzyStaffIds)) {
                Predicate in = staffId.in(fuzzyStaffIds);
                cb.in(in);
                predicate.getExpressions().add(in);
            }
            if (startEffectiveAt != null) {
                predicate.getExpressions().add(cb.greaterThanOrEqualTo(effectiveAt, startEffectiveAt));
            }
            if (endEffectiveAt != null) {
                predicate.getExpressions().add(cb.lessThanOrEqualTo(effectiveAt, endEffectiveAt));
            }
            if (CollectionUtils.isNotEmpty(authStaffIds)) {
                Predicate authIn = staffId.in(authStaffIds);
                cb.in(authIn);
                predicate.getExpressions().add(authIn);
            }

            q.where(predicate);
            return q.getRestriction();
        };
    }

    private Specification<SyncedBasicStaffInfo> getFuzzyStaffInfo(String companyId, String fuzzy) {

        return (Root<SyncedBasicStaffInfo> root, CriteriaQuery<?> q, CriteriaBuilder cb) -> {
            Predicate predicate = cb.conjunction();
            predicate.getExpressions().add(cb.equal(root.get("companyId").as(String.class), companyId));
            Expression staffName = JpaCriteriaPathUtils.getPath(root, "staffName").as(String.class);
            Expression mobileNo = JpaCriteriaPathUtils.getPath(root, "mobileNo").as(String.class);
            Expression departmentName = JpaCriteriaPathUtils.getPath(root, "departmentName").as(String.class);
            Expression idCardNo = JpaCriteriaPathUtils.getPath(root, "idCardNo").as(String.class);
            List<Predicate> fuzzyPredicates = Lists.newArrayList();
            Predicate staffNamePredicate = cb.like(staffName, fuzzy);
            Predicate mobileNoPredicate = cb.like(mobileNo, fuzzy);
            Predicate departmentNamePredicate = cb.like(departmentName, fuzzy);
            Predicate idCardNoPredicate = cb.like(idCardNo, fuzzy);
            fuzzyPredicates.add(cb.and(cb.or(staffNamePredicate, mobileNoPredicate, departmentNamePredicate, idCardNoPredicate)));

            Predicate p = cb.or(fuzzyPredicates.toArray(new Predicate[fuzzyPredicates.size()]));
            predicate.getExpressions().add(p);
            q.where(predicate);
            return q.getRestriction();
        };
    }


}
